Apache Shiro学习 - 5 - Realms

Apache Shiro学习 - 5 - Realms

Realm 是Shiro中必须的安全数据源 ( A Realm is essentially a security-specific DAO )

Realm是应用程序中获取诸如用户、角色、权限等安全数据的组件。Realm 将特定应用程序的数据转换成Shiro能够处理的格式,从而使Shiro能够提供一个单一的、易于理解的 Subject 编程API ,不论你数据源有多少、数据与特定的应用程序联系多么紧密(how application-specific your data might be)。Realm通常与数据源,如关系数据库、LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)目录、文件系统或其他类似资源,具有1对1的关系,因而, Realm 接口的实现,需要使用特定的数据源API来发现权限数据(roles, permissions等),包括 JDBC, File IO, Hibernate (MyBatis)或 JPA, 或其他 Data Access API.

由于多数数据源同时保存身份验证数据(如密码等凭证(credentials))和授权数据(如roles 或 permission),每个 Shiro 的Realm 都能够同时执行认证与授权操作( authentication and authorization operations).

Realm 配置

如果使用 Shiro的 INI 配置, 你可以像定义其他对象一样在[main]部分定义 Realms 引用,但它们有两种方式配置到一个 securityManager上去: 明确和不明确(explicitly or implicitly).

明确指定 | Explicit Assignment

基于INI配置知识我们可以知道,有一个明显的配置方法,就是定义一个或多个Realm之后,将它们作为一个集合(collection)属性配置到securityManager对象上去:

fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
securityManager.realms = $fooRealm, $barRealm, $bazRealm

明确指定是确定性的(deterministic) - 你可以精确地控制你要使用的realm及它们在验证与授权时的使用顺序。Realm顺序的影响在将在 Authentication Sequence 部分详细说明.

模糊指定 | Implicit Assignment

不推荐 Not Preferred

不明确的指定可能在你改变realm定义顺序时导致不可预测的问题。不建议使用此方法。Implicit Assignment 很有可能在将来的 Shiro 版本中移除。

如果某些原因使你不想明确地配置securityManager.realms属性, 你可以让Shiro 去探测所有配置的realm并且将它们直接指定给 securityManager .

此方法中, realm 将根据它们定义时的顺序被指定给 securityManager .

shiro.ini 例子:

blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
# no securityManager.realms assignment here

基本和加上下面这条配置的效果一样:

securityManager.realms = $blahRealm, $fooRealm, $barRealm

但需要注意的是,只有定义realm时的顺序会直接影响它们在验证和授权时如何被使用。若你修改了realm定义的顺序, 你将会修改主验证器 Authenticator的验证顺序( Authentication Sequence )功能。

因此,不建议使用 Implicit Assignment.

Realm身份验证 | Realm Authentication

一旦你理解了Shiro的身份验证工作流( Authentication workflow ),在尝试进行身份验证时清楚地知道验证器 Authenticator如何与realm交互是非常重要的。

验证令牌支持 | Supporting AuthenticationTokens

正如在 身份验证顺序 中所言, 在一个 Realm 被用来进行身份验证之前,它的 supports 方法被调用了. 只有当返回值为 true, 它的 getAuthenticationInfo(token) 方法才会被激活.

Realm通常会检查提交的令牌(token)是否是它可以处理的类型(接口或类)。例如,一个 处理生物特征资料( biometric data)的 Realm是不能处理 UsernamePasswordTokens 的, 因而它的supports 方法将会返回 false.

处理支持的认证令牌 | Handling supported AuthenticationTokens

若一个 Realm支持( supports)一个提交的令牌( AuthenticationToken),验证器( Authenticator )将会调用Realm的 getAuthenticationInfo(token) 方法. 这实际上表示了Realm后面数据源的验证尝试(This effectively represents an authentication attempt with the Realm's backing data source)。此方法的顺序是:

  1. 检查 token 中当事人(principal )的识别信息( identifying principal (账号识别信息))
  2. 基于 principal, 在数据源中查找对应的账号数据
  3. 确保令牌提供分凭证 credentials 与保存在数据中的一致
  4. 若凭证相匹配, 将返回一个以Shiro能够处理的格式封装了账号信息的 AuthenticationInfo 实例
  5. 如果凭证不匹配, 抛出 AuthenticationException 异常

这是所有Realm的 getAuthenticationInfo实现的最高级别工作流。 在此方法中,Realm可以自由地做任何处理, 例如记录登录尝试, 更新数据记录, 或其他任何与此数据源的认证尝试相关的操作.

唯一需要的是,若凭证与给定的 principal(s)匹配, 必须返回一个代表那个数据源的Subject的账号信息的非null的 AuthenticationInfo 实例。

捷径

直接实现Realm 接口将会是耗时且容易出错的工作. 大部分人都选择实现其抽象子类 AuthorizingRealm 。此类实现了基本的验证和授权工作流,节约了你的时间和精力。

凭证比对 | Credentials Matching

在上述 realm 的认证工作流中, 一个Realm必须要去核实 Subject 提交的凭证 (如密码) 必须与数据存储中的相匹配. 若它们相匹配,则验证被认为是成功的,系统核实了终端用户的身份。

Realm Credentials Matching

比对提交的凭证与Realm后面数据源中存储的凭证是每个Realm的责任而非验证器的 Authenticator 。 每个Realm 都精通凭证格式 和存储及详细的凭证匹配的知识, 而 Authenticator 只是一个工作流组件.

凭证匹配过程在所有应用程序中几乎都是一样的,唯一的区别只是比较的数据。 为确保这一过程在需要时是可插拔和可自定义的, AuthenticatingRealm 和它的子类支持凭证匹配器( CredentialsMatcher )的概念来执行凭证比对。

当获取到账号数据之后, 此数据及提交的 AuthenticationToken 将被呈现给 CredentialsMatcher 以检查提交的数据是否与存储的数据相匹配.

Shiro已有了一些 CredentialsMatcher 的实现类以便于你快速开始开发,如 SimpleCredentialsMatcherHashedCredentialsMatcher, 但若你想配置一个自己的实现类来实现自己的匹配逻辑,你可以直接这样做:

Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);

或者,使用 Shiro的 INI配置( configuration ):

[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...

简单相等比较 | Simple Equality Check

所有 Shiro 预设的(out-of-the-box) Realm 实现默认使用 SimpleCredentialsMatcher. SimpleCredentialsMatcher 对存储的账号数据与提交于 AuthenticationToken中的数据执行一种简单直接的相等比较.

例如, 若提交了一个 UsernamePasswordToken , SimpleCredentialsMatcher 会将提交的密码与存储在数据库中的密码进行精确的相等比较 .

SimpleCredentialsMatcher 执行的相等比对并不仅仅是字符串比较. 它可以执行大部分常见的字节数据源的比对,如字符串(Strings), 字符数组( character arrays), 字节数组( byte arrays),文件( Files)和输入流( InputStreams). See its JavaDoc for more.

哈希凭证 | Hashing Credentials

与以原始格式存储和比对凭证相比,一种更安全的存储终端用户凭证(如密码)的方法是在将它们存储到数据库之前对其执行单向哈希( one-way hash)。

这确保了终端用户的密码永远不会以其原始形式存储,而且无人知道其原始值。这是比文本或原始数据比对更加安全的机制,任何有安全考虑的应用程序都应该采用这种方式。

为支持这种首选的密码散列策略(preferred cryptographic hashing strategies), Shiro 提供了 HashedCredentialsMatcher 实现类的配置来代替前述的 SimpleCredentialsMatcher.

散列密码(Hashing credentials)和加盐 ( salting) 及多次哈希/散列(multiple hash iterations )的优点的讨论超出了 Realm 文档的范畴, 获取更多详细信息请参考 HashedCredentialsMatcher JavaDoc

哈希及相应的匹配器 | Hashing and Corresponding Matcher

那么,如何配置Shiro应用程序来简便的实现这种效果?

Shiro 提供了多个 HashedCredentialsMatcher 的子类实现. 你必须在你的realm中配置特定的实现类来与你用来哈希用户密码的算法相匹配。

例如, 假设你使用用户名/密码对来进行验证 。由于上述优点,你想在插件用户账户时使用 SHA-256 算法来散列用户密码。 你应该散列用户输入的文本密码,并保存其值:

import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...
//使用 Random Number Generator 来生成盐(salts). 这比不使用盐或使用用户名做盐要安全
// Shiro makes this easy.
//Note that a normal app would reference an attribute rather
//than create a new RNG every time:
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();
//Now hash the plain-text password with the random salt and multiple
//iterations and then Base64-encode the value (requires less space than Hex):
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();
User user = new User(username, hashedPasswordBase64);
//save the salt with the new account. The HashedCredentialsMatcher
//will need it later when handling login attempts:
user.setPasswordSalt(salt);
userDAO.create(user);

因为你使用了 SHA-256 来散列你的密码, 你需要告诉 Shiro 使用合适的 HashedCredentialsMatcher 来与你的选择相匹配 . 本例中, 我们创建了一个随机盐( salt )且执行了1024次迭代来增强安全 (查阅 HashedCredentialsMatcher 的 JavaDoc 了解为啥). 下面是 Shiro的相关 INI 配置:

[main]
...
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
# 本例中使用的是 base64 编码,而非 hex:
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024
# This next property is only needed in Shiro 1.0\. Shiro 1.1 及之后的版本不需要下面一行的配置:
credentialsMatcher.hashSalted = true
...
myRealm = com.company.....
myRealm.credentialsMatcher = $credentialsMatcher
...
SaltedAuthenticationInfo

确保上述设置工作的最后一件事是,你的 Realm 实现类必须返回一个 SaltedAuthenticationInfo 实例(通常是SimpleAuthenticationInfo对象)而不是一个通常的AuthenticationInfo . SaltedAuthenticationInfo 接口确保你在创建账户时使用的盐 (如上面的user.setPasswordSalt(salt); ) 可以被 HashedCredentialsMatcher使用.

HashedCredentialsMatcher 需要盐以便对提交的 AuthenticationToken 执行相同的哈希操作,来查看令牌是否与你储存的数据相匹配. 因而,若你在用户密码中加盐(你应该这样做!),请确保你的 Realm实现类返回了一个 SaltedAuthenticationInfo 实例.

禁用认证 | Disabling Authentication

若因为某些原因,你不想你的 Realm 对一个数据源进行认证(可能你只想利用Realm进行授权操作), 你应该使Realm的 supports 方法返回false来完全禁用Realm对认证的支持. 这样你的Realm永远将不会在认证尝试时被使用.

当然,若你想对Subjects进行认证,请确保最少配置了一个 Realm 来支持 AuthenticationTokens .

Realm 授权 | Realm Authorization

待续 TBD

文章目录
  1. 1. Apache Shiro学习 - 5 - Realms
    1. 1.1. Realm 配置
      1. 1.1.1. 明确指定 | Explicit Assignment
      2. 1.1.2. 模糊指定 | Implicit Assignment
    2. 1.2. Realm身份验证 | Realm Authentication
      1. 1.2.1. 验证令牌支持 | Supporting AuthenticationTokens
      2. 1.2.2. 处理支持的认证令牌 | Handling supported AuthenticationTokens
      3. 1.2.3. 凭证比对 | Credentials Matching
        1. 1.2.3.1. 简单相等比较 | Simple Equality Check
        2. 1.2.3.2. 哈希凭证 | Hashing Credentials
          1. 1.2.3.2.1. 哈希及相应的匹配器 | Hashing and Corresponding Matcher
            1. 1.2.3.2.1.1. SaltedAuthenticationInfo
      4. 1.2.4. 禁用认证 | Disabling Authentication
    3. 1.3. Realm 授权 | Realm Authorization
|